/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to you under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.calcite.sql.type;

import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeFactory;
import org.apache.calcite.rel.type.RelDataTypeFamily;
import org.apache.calcite.rel.type.RelDataTypeField;
import org.apache.calcite.rel.type.RelDataTypeFieldImpl;
import org.apache.calcite.rel.type.RelRecordType;
import org.apache.calcite.rex.RexUtil;
import org.apache.calcite.sql.SqlBasicTypeNameSpec;
import org.apache.calcite.sql.SqlCall;
import org.apache.calcite.sql.SqlCallBinding;
import org.apache.calcite.sql.SqlCollation;
import org.apache.calcite.sql.SqlCollectionTypeNameSpec;
import org.apache.calcite.sql.SqlDataTypeSpec;
import org.apache.calcite.sql.SqlIdentifier;
import org.apache.calcite.sql.SqlMapTypeNameSpec;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlRowTypeNameSpec;
import org.apache.calcite.sql.SqlTypeNameSpec;
import org.apache.calcite.sql.parser.SqlParserPos;
import org.apache.calcite.sql.validate.SqlNameMatcher;
import org.apache.calcite.sql.validate.SqlValidator;
import org.apache.calcite.sql.validate.SqlValidatorScope;
import org.apache.calcite.sql.validate.SqlValidatorUtil;
import org.apache.calcite.sql2rel.SqlToRelConverter;
import org.apache.calcite.util.NumberUtil;
import org.apache.calcite.util.Pair;
import org.apache.calcite.util.Util;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;

import org.apiguardian.api.API;
import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
import org.checkerframework.checker.nullness.qual.Nullable;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.nio.charset.Charset;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

import static com.google.common.base.Preconditions.checkArgument;

import static org.apache.calcite.rel.type.RelDataTypeImpl.NON_NULLABLE_SUFFIX;
import static org.apache.calcite.sql.type.NonNullableAccessors.getCharset;
import static org.apache.calcite.sql.type.NonNullableAccessors.getCollation;
import static org.apache.calcite.sql.type.NonNullableAccessors.getComponentTypeOrThrow;
import static org.apache.calcite.util.Static.RESOURCE;

import static java.util.Objects.requireNonNull;

/**
 * Contains utility methods used during SQL validation or type derivation.
 */
public abstract class SqlTypeUtil {
  //~ Methods ----------------------------------------------------------------

  /**
   * Checks whether two types or more are char comparable.
   *
   * @return Returns true if all operands are of char type and if they are
   * comparable, i.e. of the same charset and collation of same charset
   */
  public static boolean isCharTypeComparable(List<RelDataType> argTypes) {
    requireNonNull(argTypes, "argTypes");
    assert argTypes.size() >= 2;

    // Filter out ANY and NULL elements.
    List<RelDataType> argTypes2 = new ArrayList<>();
    for (RelDataType t : argTypes) {
      if (!isAny(t) && !isNull(t)) {
        argTypes2.add(t);
      }
    }

    for (Pair<RelDataType, RelDataType> pair : Pair.adjacents(argTypes2)) {
      RelDataType t0 = pair.left;
      RelDataType t1 = pair.right;

      if (!inCharFamily(t0) || !inCharFamily(t1)) {
        return false;
      }

      if (!getCharset(t0).equals(getCharset(t1))) {
        return false;
      }

      if (!getCollation(t0).getCharset().equals(
          getCollation(t1).getCharset())) {
        return false;
      }
    }

    return true;
  }

  /** True if there are literals with the specified data type.
   * Some data types do not have literals (e.g., UNSIGNED, ROW).
   *
   * @param type Type for literals.
   */
  public static boolean hasLiterals(RelDataType type) {
    switch (type.getSqlTypeName()) {
    case UTINYINT:
    case USMALLINT:
    case UINTEGER:
    case UBIGINT:
    case ANY:
    case SYMBOL:
    case MULTISET:
    case ARRAY:
    case MAP:
    case DISTINCT:
    case STRUCTURED:
    case ROW:
    case OTHER:
    case CURSOR:
    case COLUMN_LIST:
    case DYNAMIC_STAR:
    case GEOMETRY:
    case MEASURE:
    case FUNCTION:
    case SARG:
      return false;
    default:
      return true;
    }
  }

  /**
   * Returns whether the operands to a call are char type-comparable.
   *
   * @param binding        Binding of call to operands
   * @param operands       Operands to check for compatibility; usually the
   *                       operands of the bound call, but not always
   * @param throwOnFailure Whether to throw an exception on failure
   * @return whether operands are valid
   */
  public static boolean isCharTypeComparable(
      SqlCallBinding binding,
      List<SqlNode> operands,
      boolean throwOnFailure) {
    requireNonNull(operands, "operands");
    assert operands.size() >= 2
        : "operands.size() should be 2 or greater, actual: " + operands.size();

    if (!isCharTypeComparable(SqlTypeUtil.deriveType(binding, operands))) {
      if (throwOnFailure) {
        String msg = String.join(", ", Util.transform(operands, String::valueOf));
        throw binding.newError(RESOURCE.operandNotComparable(msg));
      }
      return false;
    }
    return true;
  }

  /** A namespace may contain multiple fields with the same name.
   * However, a proper ROW type cannot; this function will assign
   * unique names to fields when they are used to build a concrete ROW type.
   *
   * @param type A type for which type.isStruct() is true.
   * @return A new version of this type where all fields have unique names.
   *
   * <p>Note: the same rule to rename fields is used by the {@link SqlToRelConverter} later,
   * in convertNonAggregateSelectList (a private method).  This ensures that
   * the generated field names there will match with the inferred field names here.
   */
  private static RelDataType uniquify(RelDataTypeFactory factory, RelDataType type) {
    List<String> unique = SqlValidatorUtil.uniquify(type.getFieldNames(), true);
    List<RelDataType> types = type.getFieldList()
        .stream()
        .map(RelDataTypeField::getType)
        .collect(Collectors.toList());
    return factory.createStructType(type.getStructKind(), types, unique);
  }

  /**
   * Derives component type for ARRAY, MULTISET, MAP when input is sub-query.
   *
   * @param factory Type factory used to generate new types if necessary
   * @param origin original component type
   * @return component type
   */
  public static RelDataType deriveCollectionQueryComponentType(
      RelDataTypeFactory factory,
      SqlTypeName collectionType,
      RelDataType origin) {
    switch (collectionType) {
    case ARRAY:
    case MULTISET:
      return origin.isStruct() && origin.getFieldCount() == 1
          ? origin.getFieldList().get(0).getType() : uniquify(factory, origin);
    case MAP:
      return origin;
    default:
      throw new AssertionError(
          "Impossible to derive component type for " + collectionType);
    }
  }

  /**
   * Derives component type for ARRAY, MULTISET, MAP when input is sub-query.
   *
   * @param origin original component type
   * @return component type
   * @deprecated Use
   * {@link SqlTypeUtil#deriveCollectionQueryComponentType(RelDataTypeFactory, SqlTypeName, RelDataType)}
   */
  @Deprecated
  public static RelDataType deriveCollectionQueryComponentType(
      SqlTypeName collectionType,
      RelDataType origin) {
    switch (collectionType) {
    case ARRAY:
    case MULTISET:
      return origin.isStruct() && origin.getFieldCount() == 1
          ? origin.getFieldList().get(0).getType() : origin;
    case MAP:
      return origin;
    default:
      throw new AssertionError(
          "Impossible to derive component type for " + collectionType);
    }
  }

  /**
   * Iterates over all operands, derives their types, and collects them into
   * a list.
   */
  public static List<RelDataType> deriveAndCollectTypes(
      SqlValidator validator,
      SqlValidatorScope scope,
      List<? extends SqlNode> operands) {
    // NOTE: Do not use an AbstractList. Don't want to be lazy. We want
    // errors.
    List<RelDataType> types = new ArrayList<>();
    for (SqlNode operand : operands) {
      types.add(validator.deriveType(scope, operand));
    }
    return types;
  }

  /**
   * Derives type of the call via its binding.
   *
   * @param binding binding to derive the type from
   * @return datatype of the call
   */
  @API(since = "1.26", status = API.Status.EXPERIMENTAL)
  public static RelDataType deriveType(SqlCallBinding binding) {
    return deriveType(binding, binding.getCall());
  }

  /**
   * Derives type of the given call under given binding.
   *
   * @param binding binding to derive the type from
   * @param node node type to derive
   * @return datatype of the given node
   */
  @API(since = "1.26", status = API.Status.EXPERIMENTAL)
  public static RelDataType deriveType(SqlCallBinding binding, SqlNode node) {
    return binding.getValidator().deriveType(
        requireNonNull(binding.getScope(), () -> "scope of " + binding), node);
  }

  /**
   * Derives types for the list of nodes.
   *
   * @param binding binding to derive the type from
   * @param nodes the list of nodes to derive types from
   * @return the list of types of the given nodes
   */
  @API(since = "1.26", status = API.Status.EXPERIMENTAL)
  public static List<RelDataType> deriveType(SqlCallBinding binding,
      List<? extends SqlNode> nodes) {
    return deriveAndCollectTypes(
        binding.getValidator(),
        requireNonNull(binding.getScope(), () -> "scope of " + binding),
        nodes);
  }

  /**
   * Promotes a type to a row type (does nothing if it already is one).
   *
   * @param type      type to be promoted
   * @param fieldName name to give field in row type; null for default of
   *                  "ROW_VALUE"
   * @return row type
   */
  public static RelDataType promoteToRowType(
      RelDataTypeFactory typeFactory,
      RelDataType type,
      @Nullable String fieldName) {
    if (!type.isStruct()) {
      if (fieldName == null) {
        fieldName = "ROW_VALUE";
      }
      type = typeFactory.builder().add(fieldName, type).build();
    }
    return type;
  }

  /**
   * Recreates a given RelDataType with nullability iff any of the operands
   * of a call are nullable.
   */
  public static RelDataType makeNullableIfOperandsAre(
      final SqlValidator validator,
      final SqlValidatorScope scope,
      final SqlCall call,
      RelDataType type) {
    for (SqlNode operand : call.getOperandList()) {
      RelDataType operandType = validator.deriveType(scope, operand);

      if (containsNullable(operandType)) {
        RelDataTypeFactory typeFactory = validator.getTypeFactory();
        type = typeFactory.createTypeWithNullability(type, true);
        break;
      }
    }
    return type;
  }

  /**
   * Recreates a given RelDataType with nullability iff any of the param
   * argTypes are nullable.
   */
  public static RelDataType makeNullableIfOperandsAre(
      final RelDataTypeFactory typeFactory,
      final List<RelDataType> argTypes,
      RelDataType type) {
    requireNonNull(type, "type");
    if (containsNullable(argTypes)) {
      type = typeFactory.createTypeWithNullability(type, true);
    }
    return type;
  }

  /**
   * Returns whether all of array of types are nullable.
   */
  public static boolean allNullable(List<RelDataType> types) {
    for (RelDataType type : types) {
      if (!containsNullable(type)) {
        return false;
      }
    }
    return true;
  }

  /**
   * Returns whether one or more of an array of types is nullable.
   */
  public static boolean containsNullable(List<RelDataType> types) {
    for (RelDataType type : types) {
      if (containsNullable(type)) {
        return true;
      }
    }
    return false;
  }

  /**
   * Determines whether a type or any of its fields (if a structured type) are
   * nullable.
   */
  public static boolean containsNullable(RelDataType type) {
    if (type.isNullable()) {
      return true;
    }
    if (!type.isStruct()) {
      return false;
    }
    for (RelDataTypeField field : type.getFieldList()) {
      if (containsNullable(field.getType())) {
        return true;
      }
    }
    return false;
  }

  /**
   * Creates a RelDataType having the same type of the sourceRelDataType,
   * and the same nullability as the targetRelDataType.
   */
  public static RelDataType keepSourceTypeAndTargetNullability(RelDataType sourceRelDataType,
                                             RelDataType targetRelDataType,
                                             RelDataTypeFactory typeFactory) {
    checkArgument(
        (targetRelDataType.isStruct() && sourceRelDataType.isStruct())
            || (!targetRelDataType.isStruct() && !sourceRelDataType.isStruct()),
        "one is a struct, while the other one is not");
    if (!targetRelDataType.isStruct()) {
      return typeFactory.createTypeWithNullability(
              sourceRelDataType, targetRelDataType.isNullable());
    }
    List<RelDataTypeField> targetFields = targetRelDataType.getFieldList();
    List<RelDataTypeField> sourceFields = sourceRelDataType.getFieldList();
    ImmutableList.Builder<RelDataTypeField> newTargetField = ImmutableList.builder();
    for (int i = 0; i < targetRelDataType.getFieldCount(); i++) {
      RelDataTypeField targetField = targetFields.get(i);
      RelDataTypeField sourceField = sourceFields.get(i);
      newTargetField.add(
          new RelDataTypeFieldImpl(
              sourceField.getName(),
              sourceField.getIndex(),
                  keepSourceTypeAndTargetNullability(
                          sourceField.getType(), targetField.getType(), typeFactory)));
    }
    RelDataType relDataType = typeFactory.createStructType(newTargetField.build());
    return typeFactory.createTypeWithNullability(relDataType, targetRelDataType.isNullable());
  }

  /**
   * Returns typeName.equals(type.getSqlTypeName()). If
   * typeName.equals(SqlTypeName.Any) true is always returned.
   */
  public static boolean isOfSameTypeName(
      SqlTypeName typeName,
      RelDataType type) {
    return SqlTypeName.ANY == typeName
        || typeName == type.getSqlTypeName();
  }

  /**
   * Returns true if any element in <code>typeNames</code> matches
   * type.getSqlTypeName().
   *
   * @see #isOfSameTypeName(SqlTypeName, RelDataType)
   */
  public static boolean isOfSameTypeName(
      Collection<SqlTypeName> typeNames,
      RelDataType type) {
    for (SqlTypeName typeName : typeNames) {
      if (isOfSameTypeName(typeName, type)) {
        return true;
      }
    }
    return false;
  }

  /** Returns whether a type is DATE, TIME, or TIMESTAMP. */
  public static boolean isDatetime(RelDataType type) {
    return SqlTypeFamily.DATETIME.contains(type);
  }

  /** Returns whether a type is DATE. */
  public static boolean isDate(RelDataType type) {
    SqlTypeName typeName = type.getSqlTypeName();
    if (typeName == null) {
      return false;
    }

    return type.getSqlTypeName() == SqlTypeName.DATE;
  }

  /** Returns whether a type is TIMESTAMP. */
  public static boolean isTimestamp(RelDataType type) {
    return SqlTypeFamily.TIMESTAMP.contains(type);
  }

  /** Returns whether a type is some kind of INTERVAL. */
  @SuppressWarnings("contracts.conditional.postcondition.not.satisfied")
  @EnsuresNonNullIf(expression = "#1.getIntervalQualifier()", result = true)
  public static boolean isInterval(RelDataType type) {
    return SqlTypeFamily.DATETIME_INTERVAL.contains(type);
  }

  /** Returns whether a type is in SqlTypeFamily.Character. */
  @SuppressWarnings("contracts.conditional.postcondition.not.satisfied")
  @EnsuresNonNullIf(expression = "#1.getCharset()", result = true)
  @EnsuresNonNullIf(expression = "#1.getCollation()", result = true)
  public static boolean inCharFamily(RelDataType type) {
    return type.getFamily() == SqlTypeFamily.CHARACTER;
  }

  /** Returns whether a type name is in SqlTypeFamily.Character. */
  public static boolean inCharFamily(SqlTypeName typeName) {
    return typeName.getFamily() == SqlTypeFamily.CHARACTER;
  }

  /** Returns whether a type is in SqlTypeFamily.Boolean. */
  public static boolean inBooleanFamily(RelDataType type) {
    return type.getFamily() == SqlTypeFamily.BOOLEAN;
  }

  /** Returns whether two types are in same type family. */
  public static boolean inSameFamily(RelDataType t1, RelDataType t2) {
    return t1.getFamily() == t2.getFamily();
  }

  /** Returns whether two types are in same type family, or one or the other is
   * of type {@link SqlTypeName#NULL}. */
  public static boolean inSameFamilyOrNull(RelDataType t1, RelDataType t2) {
    return (t1.getSqlTypeName() == SqlTypeName.NULL)
        || (t2.getSqlTypeName() == SqlTypeName.NULL)
        || (t1.getFamily() == t2.getFamily());
  }

  /** Returns whether a type family is either character or binary. */
  public static boolean inCharOrBinaryFamilies(RelDataType type) {
    return (type.getFamily() == SqlTypeFamily.CHARACTER)
        || (type.getFamily() == SqlTypeFamily.BINARY);
  }

  /** Returns whether a type is a LOB of some kind. */
  public static boolean isLob(RelDataType type) {
    // TODO jvs 9-Dec-2004:  once we support LOB types
    return false;
  }

  /** Returns whether a type is variable width with bounded precision. */
  public static boolean isBoundedVariableWidth(RelDataType type) {
    SqlTypeName typeName = type.getSqlTypeName();
    if (typeName == null) {
      return false;
    }
    switch (typeName) {
    case VARCHAR:
    case VARBINARY:

      // TODO angel 8-June-2005: Multiset should be LOB
    case MULTISET:
      return true;
    default:
      return false;
    }
  }

  /** Returns whether a type is one of the integer types. */
  public static boolean isIntType(RelDataType type) {
    SqlTypeName typeName = type.getSqlTypeName();
    if (typeName == null) {
      return false;
    }
    switch (typeName) {
    case TINYINT:
    case SMALLINT:
    case INTEGER:
    case BIGINT:
    case UTINYINT:
    case USMALLINT:
    case UINTEGER:
    case UBIGINT:
      return true;
    default:
      return false;
    }
  }

  /** Returns whether a type is DECIMAL. */
  public static boolean isDecimal(RelDataType type) {
    SqlTypeName typeName = type.getSqlTypeName();
    if (typeName == null) {
      return false;
    }
    return typeName == SqlTypeName.DECIMAL;
  }

  /** Returns whether a type is numeric with exact precision. */
  public static boolean isExactNumeric(RelDataType type) {
    SqlTypeName typeName = type.getSqlTypeName();
    if (typeName == null) {
      return false;
    }
    switch (typeName) {
    case TINYINT:
    case SMALLINT:
    case INTEGER:
    case BIGINT:
    case UTINYINT:
    case USMALLINT:
    case UINTEGER:
    case UBIGINT:
    case DECIMAL:
      return true;
    default:
      return false;
    }
  }

  /**
   * Returns whether {@code container} can represent every value produced by
   * {@code content} without loss of information.
   *
   * <p>The {@code container} type must be one of the integer types (signed or
   * unsigned). The {@code content} type can be integer, or a DECIMAL with
   * scale {@code 0}. For all other types this method returns {@code false}.
   *
   * @throws IllegalArgumentException if {@code container} is not an integer type
   */
  public static boolean integerRangeContains(RelDataType container, RelDataType content) {
    checkArgument(isIntType(container),
        "container must be an integer type: %s", container);

    final SqlTypeName contentType = content.getSqlTypeName();
    final boolean contentIsDecimal = contentType == SqlTypeName.DECIMAL;
    if (!isIntType(content) && (!contentIsDecimal || content.getScale() != 0)) {
      return false;
    }

    final BigInteger containerMin = integerBound(container, false);
    final BigInteger containerMax = integerBound(container, true);
    if (containerMin == null || containerMax == null) {
      return false;
    }

    final BigInteger contentMin = integerBound(content, false);
    final BigInteger contentMax = integerBound(content, true);
    if (contentMin == null || contentMax == null) {
      return false;
    }

    return containerMin.compareTo(contentMin) <= 0
        && containerMax.compareTo(contentMax) >= 0;
  }

  /**
   * Returns the numeric bound for an integer or zero-scale decimal type.
   *
   * @param type Type whose bounds should be computed
   * @param upper If {@code true}, returns the maximum inclusive bound;
   *              otherwise returns the minimum bound
   * @return Bound as {@link BigInteger}, or {@code null} if the bound cannot be
   *         determined (for example, type is not integer or has non-zero scale)
   */
  public static @Nullable BigInteger integerBound(RelDataType type, boolean upper) {
    final SqlTypeName typeName = type.getSqlTypeName();

    final boolean isDecimal = typeName == SqlTypeName.DECIMAL;
    if (!isDecimal && !isIntType(type)) {
      return null;
    }
    if (isDecimal && type.getScale() != 0) {
      return null;
    }

    final int precision = isDecimal ? type.getPrecision() : -1;
    final int scale = isDecimal ? type.getScale() : -1;
    final Object limit =
        typeName.getLimit(upper, SqlTypeName.Limit.OVERFLOW,
        false,
        precision,
        scale);
    if (limit == null) {
      return null;
    }
    if (limit instanceof BigDecimal) {
      try {
        return ((BigDecimal) limit).toBigIntegerExact();
      } catch (ArithmeticException ignored) {
        return null;
      }
    }
    if (limit instanceof Number) {
      return BigInteger.valueOf(((Number) limit).longValue());
    }
    return null;
  }

  /** Returns whether a type's scale is set. */
  public static boolean hasScale(RelDataType type) {
    return type.getScale() != Integer.MIN_VALUE;
  }

  /** Returns the maximum value of an integral type, as a long value.
   * DOES NOT WORK FOR UBIGINT. */
  public static long maxValue(RelDataType type) {
    assert SqlTypeUtil.isIntType(type);
    switch (type.getSqlTypeName()) {
    case TINYINT:
      return Byte.MAX_VALUE;
    case SMALLINT:
      return Short.MAX_VALUE;
    case INTEGER:
      return Integer.MAX_VALUE;
    case UTINYINT:
      return 255;
    case USMALLINT:
      return 65535;
    case UINTEGER:
      return (1L << 32) - 1;
    case BIGINT:
      return Long.MAX_VALUE;
    default:
      throw Util.unexpected(type.getSqlTypeName());
    }
  }

  /** Returns whether a type is numeric with approximate precision. */
  public static boolean isApproximateNumeric(RelDataType type) {
    SqlTypeName typeName = type.getSqlTypeName();
    if (typeName == null) {
      return false;
    }
    switch (typeName) {
    case FLOAT:
    case REAL:
    case DOUBLE:
      return true;
    default:
      return false;
    }
  }

  /** Returns whether a type is numeric. */
  public static boolean isNumeric(RelDataType type) {
    return isExactNumeric(type) || isApproximateNumeric(type);
  }

  /** Returns whether a type is the NULL type. */
  public static boolean isNull(RelDataType type) {
    SqlTypeName typeName = type.getSqlTypeName();
    if (typeName == null) {
      return false;
    }
    return typeName == SqlTypeName.NULL;
  }

  /**
   * Tests whether two types have the same name and structure, possibly with
   * differing modifiers. For example, VARCHAR(1) and VARCHAR(10) are
   * considered the same, while VARCHAR(1) and CHAR(1) are considered
   * different. Likewise, VARCHAR(1) MULTISET and VARCHAR(10) MULTISET are
   * considered the same.
   *
   * @return true if types have same name and structure
   */
  public static boolean sameNamedType(RelDataType t1, RelDataType t2) {
    if (t1.isStruct() || t2.isStruct()) {
      if (!t1.isStruct() || !t2.isStruct()) {
        return false;
      }
      if (t1.getFieldCount() != t2.getFieldCount()) {
        return false;
      }
      List<RelDataTypeField> fields1 = t1.getFieldList();
      List<RelDataTypeField> fields2 = t2.getFieldList();
      for (int i = 0; i < fields1.size(); ++i) {
        if (!sameNamedType(
            fields1.get(i).getType(),
            fields2.get(i).getType())) {
          return false;
        }
      }
      return true;
    }
    SqlTypeName t1Name = t1.getSqlTypeName();
    SqlTypeName t2Name = t2.getSqlTypeName();
    if (t1Name == SqlTypeName.ARRAY || t1Name == SqlTypeName.MULTISET) {
      if (t1Name != t2Name) {
        return false;
      }

      RelDataType comp1 = requireNonNull(t1.getComponentType());
      RelDataType comp2 = requireNonNull(t2.getComponentType());
      return sameNamedType(comp1, comp2);
    }

    if (t1Name == SqlTypeName.MAP) {
      if (t1Name != t2Name) {
        return false;
      }

      RelDataType keyType1 = requireNonNull(t1.getKeyType());
      RelDataType keyType2 = requireNonNull(t2.getKeyType());
      if (!sameNamedType(keyType1, keyType2)) {
        return false;
      }
      RelDataType valueType1 = requireNonNull(t1.getValueType());
      RelDataType valueType2 = requireNonNull(t2.getValueType());
      return sameNamedType(valueType1, valueType2);
    }

    return t1Name == t2Name;
  }

  /**
   * Computes the maximum number of bytes required to represent a value of a
   * type having user-defined precision. This computation assumes no overhead
   * such as length indicators and NUL-terminators. Complex types for which
   * multiple representations are possible (e.g. DECIMAL or TIMESTAMP) return
   * 0.
   *
   * @param type type for which to compute storage
   * @return maximum bytes, or 0 for a fixed-width type or type with unknown
   * maximum
   */
  public static int getMaxByteSize(RelDataType type) {
    SqlTypeName typeName = type.getSqlTypeName();

    if (typeName == null) {
      return 0;
    }

    switch (typeName) {
    case CHAR:
    case VARCHAR:
      return (int) Math.ceil(
          ((double) type.getPrecision())
              * getCharset(type).newEncoder().maxBytesPerChar());

    case BINARY:
    case VARBINARY:
      return type.getPrecision();

    case MULTISET:

      // TODO Wael Jan-24-2005: Need a better way to tell fennel this
      // number. This a very generic place and implementation details like
      // this doesnt belong here. Waiting to change this once we have blob
      // support
      return 4096;

    default:
      return 0;
    }
  }

  /** Returns the minimum unscaled value of a numeric type.
   *
   * @deprecated Use {@link #integerBound(RelDataType, boolean)} with {@code upper = false}
   */
  @Deprecated // to be removed before 2.0
  public static long getMinValue(RelDataType type) {
    SqlTypeName typeName = type.getSqlTypeName();
    switch (typeName) {
    case TINYINT:
      return Byte.MIN_VALUE;
    case SMALLINT:
      return Short.MIN_VALUE;
    case INTEGER:
      return Integer.MIN_VALUE;
    case UTINYINT:
    case USMALLINT:
    case UINTEGER:
    case UBIGINT:
      return 0;
    case BIGINT:
    case DECIMAL:
      return NumberUtil.getMinUnscaled(type.getPrecision()).longValue();
    default:
      throw new AssertionError("getMinValue(" + typeName + ")");
    }
  }

  /** Returns the maximum unscaled value of a numeric type.
   * DOES NOT WORK CORRECTLY FOR U/BIGINT and many DECIMAL types.
   *
   * @deprecated Use {@link #integerBound(RelDataType, boolean)} with {@code upper = true}
   */
  @Deprecated // to be removed before 2.0
  public static long getMaxValue(RelDataType type) {
    SqlTypeName typeName = type.getSqlTypeName();
    switch (typeName) {
    case UTINYINT:
      return 255;
    case TINYINT:
      return Byte.MAX_VALUE;
    case USMALLINT:
      return (1 << 16) - 1;
    case SMALLINT:
      return Short.MAX_VALUE;
    case UINTEGER:
      return (1L << 32) - 1;
    case INTEGER:
      return Integer.MAX_VALUE;
    case BIGINT:
    case DECIMAL:
      return NumberUtil.getMaxUnscaled(type.getPrecision()).longValue();
    default:
      throw new AssertionError("getMaxValue(" + typeName + ")");
    }
  }

  /** Returns whether a type has a representation as a Java primitive (ignoring
   * nullability). */
  @Deprecated // to be removed before 2.0
  public static boolean isJavaPrimitive(RelDataType type) {
    SqlTypeName typeName = type.getSqlTypeName();
    if (typeName == null) {
      return false;
    }

    switch (typeName) {
    case BOOLEAN:
    case TINYINT:
    case SMALLINT:
    case INTEGER:
    case BIGINT:
    case FLOAT:
    case REAL:
    case DOUBLE:
    case SYMBOL:
      return true;
    default:
      return false;
    }
  }

  /** Returns the class name of the wrapper for the primitive data type. */
  @Deprecated // to be removed before 2.0
  public static @Nullable String getPrimitiveWrapperJavaClassName(@Nullable RelDataType type) {
    if (type == null) {
      return null;
    }
    SqlTypeName typeName = type.getSqlTypeName();
    if (typeName == null) {
      return null;
    }

    switch (typeName) {
    case BOOLEAN:
      return "Boolean";
    default:
      //noinspection deprecation
      return getNumericJavaClassName(type);
    }
  }

  /** Returns the class name of a numeric data type. */
  @Deprecated // to be removed before 2.0
  public static @Nullable String getNumericJavaClassName(@Nullable RelDataType type) {
    if (type == null) {
      return null;
    }
    SqlTypeName typeName = type.getSqlTypeName();
    if (typeName == null) {
      return null;
    }

    switch (typeName) {
    case TINYINT:
      return "Byte";
    case SMALLINT:
      return "Short";
    case INTEGER:
      return "Integer";
    case BIGINT:
      return "Long";
    case REAL:
      return "Float";
    case DECIMAL:
    case FLOAT:
    case DOUBLE:
      return "Double";
    default:
      return null;
    }
  }

  private static boolean isAny(RelDataType t) {
    return t.getFamily() == SqlTypeFamily.ANY;
  }

  private static boolean isVariant(RelDataType t) {
    return t.getFamily() == SqlTypeFamily.VARIANT;
  }

  public static boolean isMeasure(RelDataType t) {
    return t instanceof MeasureSqlType;
  }

  /**
   * Tests whether a value can be assigned to a site.
   *
   * @param toType   type of the target site
   * @param fromType type of the source value
   * @return true iff assignable
   */
  public static boolean canAssignFrom(
      RelDataType toType,
      RelDataType fromType) {
    if (isAny(toType) || isAny(fromType)) {
      return true;
    }

    // TODO jvs 2-Jan-2005:  handle all the other cases like
    // rows, collections, UDT's
    if (fromType.getSqlTypeName() == SqlTypeName.NULL) {
      // REVIEW jvs 4-Dec-2008: We allow assignment from NULL to any
      // type, including NOT NULL types, since in the case where no
      // rows are actually processed, the assignment is legal
      // (FRG-365).  However, it would be better if the validator's
      // NULL type inference guaranteed that we had already
      // assigned a real (nullable) type to every NULL literal.
      return true;
    }

    if (fromType.getSqlTypeName() == SqlTypeName.ARRAY) {
      if (toType.getSqlTypeName() != SqlTypeName.ARRAY) {
        return false;
      }
      return canAssignFrom(getComponentTypeOrThrow(toType), getComponentTypeOrThrow(fromType));
    }

    if (areCharacterSetsMismatched(toType, fromType)) {
      return false;
    }

    return toType.getFamily() == fromType.getFamily();
  }

  /**
   * Determines whether two types both have different character sets. If one
   * or the other type has no character set (e.g. in cast from INT to
   * VARCHAR), that is not a mismatch.
   *
   * @param t1 first type
   * @param t2 second type
   * @return true iff mismatched
   */
  public static boolean areCharacterSetsMismatched(
      RelDataType t1,
      RelDataType t2) {
    if (isAny(t1) || isAny(t2)) {
      return false;
    }

    Charset cs1 = t1.getCharset();
    Charset cs2 = t2.getCharset();
    if ((cs1 != null) && (cs2 != null)) {
      if (!cs1.equals(cs2)) {
        return true;
      }
    }
    return false;
  }

  /**
   * Compares two types and returns whether {@code fromType} can be cast to
   * {@code toType}, using either coercion or assignment.
   *
   * <p>REVIEW jvs 17-Dec-2004: the coerce param below shouldn't really be
   * necessary. We're using it as a hack because
   * {@link SqlTypeFactoryImpl#leastRestrictive} isn't complete enough
   * yet.  Once it is, this param (and the non-coerce rules of
   * {@link SqlTypeAssignmentRule}) should go away.
   *
   * @param toType   target of assignment
   * @param fromType source of assignment
   * @param coerce   if true, the SQL rules for CAST are used; if false, the
   *                 rules are similar to Java; e.g. you can't assign short x =
   *                 (int) y, and you can't assign int x = (String) z.
   * @return whether cast is legal
   */
  public static boolean canCastFrom(
      RelDataType toType,
      RelDataType fromType,
      boolean coerce) {
    return canCastFrom(toType, fromType,
        coerce ? SqlTypeCoercionRule.instance()
            : SqlTypeAssignmentRule.instance());
  }

  /**
   * Compares two types and returns whether {@code fromType} can be cast to
   * {@code toType}.
   *
   * <p>A type mapping rule (i.e. {@link SqlTypeCoercionRule}
   * or {@link SqlTypeAssignmentRule}) controls what types are allowed to be
   * cast from/to.
   *
   * @param toType   target of assignment
   * @param fromType source of assignment
   * @param typeMappingRule  SqlTypeMappingRule
   *
   * @return whether cast is legal
   */
  public static boolean canCastFrom(
      RelDataType toType,
      RelDataType fromType,
      SqlTypeMappingRule typeMappingRule) {
    if (toType.equals(fromType)) {
      return true;
    }
    // If fromType is a measure, you should compare the inner type otherwise it will
    // always return TRUE because the SqlTypeFamily of MEASURE is ANY
    if (isMeasure(fromType)) {
      return canCastFrom(toType, requireNonNull(fromType.getMeasureElementType()),
                         typeMappingRule);
    }
    if (isAny(toType) || isAny(fromType) || isVariant(toType) || isVariant(fromType)) {
      return true;
    }

    final SqlTypeName fromTypeName = fromType.getSqlTypeName();
    final SqlTypeName toTypeName = toType.getSqlTypeName();
    if (toTypeName == SqlTypeName.UNKNOWN) {
      return true;
    }
    if (toType.getSqlTypeName() == SqlTypeName.UUID) {
      return fromType.getSqlTypeName() == SqlTypeName.NULL
          || fromType.getSqlTypeName() == SqlTypeName.UUID
          || fromType.getFamily() == SqlTypeFamily.CHARACTER
          || fromType.getFamily() == SqlTypeFamily.BINARY;
    }
    if (toType.isStruct() || fromType.isStruct()) {
      if (toTypeName == SqlTypeName.DISTINCT) {
        if (fromTypeName == SqlTypeName.DISTINCT) {
          // can't cast between different distinct types
          return false;
        }
        return canCastFrom(
            toType.getFieldList().get(0).getType(), fromType, typeMappingRule);
      } else if (fromTypeName == SqlTypeName.DISTINCT) {
        return canCastFrom(
            toType, fromType.getFieldList().get(0).getType(), typeMappingRule);
      } else if (toTypeName == SqlTypeName.ROW) {
        if (fromTypeName != SqlTypeName.ROW) {
          return fromTypeName == SqlTypeName.NULL;
        }
        int n = toType.getFieldCount();
        if (fromType.getFieldCount() != n) {
          return false;
        }
        for (int i = 0; i < n; ++i) {
          RelDataTypeField toField = toType.getFieldList().get(i);
          RelDataTypeField fromField = fromType.getFieldList().get(i);
          if (!canCastFrom(
              toField.getType(),
              fromField.getType(),
              typeMappingRule)) {
            return false;
          }
        }
        return true;
      } else if (toTypeName == SqlTypeName.MULTISET) {
        if (!fromType.isStruct()) {
          return false;
        }
        if (fromTypeName != SqlTypeName.MULTISET) {
          return false;
        }
        return canCastFrom(
            getComponentTypeOrThrow(toType),
            getComponentTypeOrThrow(fromType),
            typeMappingRule);
      } else if (fromTypeName == SqlTypeName.MULTISET) {
        return false;
      } else {
        return toType.getFamily() == fromType.getFamily();
      }
    }
    RelDataType c1 = toType.getComponentType();
    if (c1 != null) {
      RelDataType c2 = fromType.getComponentType();
      if (c2 != null) {
        return canCastFrom(c1, c2, typeMappingRule);
      }
    }
    if ((isInterval(fromType) && isExactNumeric(toType))
        || (isInterval(toType) && isExactNumeric(fromType))) {
      IntervalSqlType intervalType =
          (IntervalSqlType) (isInterval(fromType) ? fromType : toType);
      if (!intervalType.getIntervalQualifier().isSingleDatetimeField()) {
        // Casts between intervals and exact numerics must involve
        // intervals with a single datetime field.
        return false;
      }
    }
    if (toTypeName == null || fromTypeName == null) {
      return false;
    }
    return typeMappingRule.canApplyFrom(toTypeName, fromTypeName);
  }

  /**
   * Flattens a record type by recursively expanding any fields which are
   * themselves record types. For each record type, a representative null
   * value field is also prepended (with state NULL for a null value and FALSE
   * for non-null), and all component types are asserted to be nullable, since
   * SQL doesn't allow NOT NULL to be specified on attributes.
   *
   * @param typeFactory   factory which should produced flattened type
   * @param recordType    type with possible nesting
   * @param flatteningMap if non-null, receives map from unflattened ordinal
   *                      to flattened ordinal (must have length at least
   *                      recordType.getFieldList().size())
   * @return flattened equivalent
   */
  public static RelDataType flattenRecordType(
      RelDataTypeFactory typeFactory,
      RelDataType recordType,
      int @Nullable [] flatteningMap) {
    if (!recordType.isStruct()) {
      return recordType;
    }
    List<RelDataTypeField> fieldList = new ArrayList<>();
    boolean nested =
        flattenFields(
            typeFactory,
            recordType,
            fieldList,
            flatteningMap);
    if (!nested) {
      return recordType;
    }
    List<RelDataType> types = new ArrayList<>();
    List<String> fieldNames = new ArrayList<>();
    Map<String, Long> fieldCnt = fieldList.stream()
        .map(RelDataTypeField::getName)
        .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
    int i = -1;
    for (RelDataTypeField field : fieldList) {
      ++i;
      types.add(field.getType());
      String oriFieldName = field.getName();
      // Patch up the field name with index if there are duplicates.
      // There is still possibility that the patched name conflicts with existing ones,
      // but that should be rare case.
      Long fieldCount = fieldCnt.get(oriFieldName);
      String fieldName = fieldCount != null && fieldCount > 1
          ? oriFieldName + "_" + i
          : oriFieldName;
      fieldNames.add(fieldName);
    }
    return typeFactory.createStructType(types, fieldNames);
  }

  public static boolean needsNullIndicator(RelDataType recordType) {
    // NOTE jvs 9-Mar-2005: It would be more storage-efficient to say that
    // no null indicator is required for structured type columns declared
    // as NOT NULL.  However, the uniformity of always having a null
    // indicator makes things cleaner in many places.
    return recordType.getSqlTypeName() == SqlTypeName.STRUCTURED;
  }

  private static boolean flattenFields(
      RelDataTypeFactory typeFactory,
      RelDataType type,
      List<RelDataTypeField> list,
      int @Nullable [] flatteningMap) {
    boolean nested = false;
    for (RelDataTypeField field : type.getFieldList()) {
      if (flatteningMap != null) {
        flatteningMap[field.getIndex()] = list.size();
      }
      if (field.getType().isStruct()) {
        nested = true;
        flattenFields(
            typeFactory,
            field.getType(),
            list,
            null);
      } else if (field.getType().getComponentType() != null) {
        nested = true;

        // TODO jvs 14-Feb-2005:  generalize to any kind of
        // collection type
        RelDataType flattenedCollectionType =
            typeFactory.createMultisetType(
                flattenRecordType(
                    typeFactory,
                    getComponentTypeOrThrow(field.getType()),
                    null),
                -1);
        if (field.getType() instanceof ArraySqlType) {
          flattenedCollectionType =
              typeFactory.createArrayType(
                  flattenRecordType(
                      typeFactory,
                      getComponentTypeOrThrow(field.getType()),
                      null),
                  -1);
        }
        field =
            new RelDataTypeFieldImpl(
                field.getName(),
                field.getIndex(),
                flattenedCollectionType);
        list.add(field);
      } else {
        list.add(field);
      }
    }
    return nested;
  }

  /**
   * Converts an instance of RelDataType to an instance of SqlDataTypeSpec.
   *
   * @param type         type descriptor
   * @param charSetName  charSet name
   * @param maxPrecision The max allowed precision.
   * @param maxScale     max allowed scale
   * @return corresponding parse representation
   */
  public static SqlDataTypeSpec convertTypeToSpec(RelDataType type,
      @Nullable String charSetName, int maxPrecision, int maxScale) {
    SqlTypeName typeName = type.getSqlTypeName();

    // TODO jvs 28-Dec-2004:  support row types, user-defined types,
    // interval types, multiset types, etc
    requireNonNull(typeName, "typeName");

    final SqlTypeNameSpec typeNameSpec;
    if (isAtomic(type) || isNull(type)
        || type.getSqlTypeName() == SqlTypeName.UNKNOWN
        || type.getSqlTypeName() == SqlTypeName.GEOMETRY
        || SqlTypeUtil.isInterval(type)) {
      int precision =
          typeName.allowsPrec() ? type.getPrecision() : RelDataType.PRECISION_NOT_SPECIFIED;
      // fix up the precision.
      if (maxPrecision > 0 && precision > maxPrecision) {
        precision = maxPrecision;
      }
      int scale = typeName.allowsScale() ? type.getScale() :  RelDataType.SCALE_NOT_SPECIFIED;
      if (maxScale > 0 && scale > maxScale) {
        scale = maxScale;
      }

      typeNameSpec =
          new SqlBasicTypeNameSpec(typeName, precision, scale, charSetName,
              SqlParserPos.ZERO);
    } else if (isCollection(type)) {
      RelDataType componentType = getComponentTypeOrThrow(type);
      typeNameSpec =
          new SqlCollectionTypeNameSpec(
              convertTypeToSpec(componentType).getTypeNameSpec(),
              componentType.isNullable(),
              typeName, SqlParserPos.ZERO);
    } else if (isRow(type)) {
      RelRecordType recordType = (RelRecordType) type;
      List<RelDataTypeField> fields = recordType.getFieldList();
      List<SqlIdentifier> fieldNames = fields.stream()
          .map(f -> new SqlIdentifier(f.getName(), SqlParserPos.ZERO))
          .collect(Collectors.toList());
      List<SqlDataTypeSpec> fieldTypes = fields.stream()
          .map(f -> convertTypeToSpec(f.getType()))
          .collect(Collectors.toList());
      typeNameSpec = new SqlRowTypeNameSpec(SqlParserPos.ZERO, fieldNames, fieldTypes);
    } else if (isMap(type)) {
      final RelDataType keyType =
          requireNonNull(type.getKeyType(), () -> "keyType of " + type);
      final RelDataType valueType =
          requireNonNull(type.getValueType(), () -> "valueType of " + type);
      final SqlDataTypeSpec keyTypeSpec = convertTypeToSpec(keyType);
      final SqlDataTypeSpec valueTypeSpec = convertTypeToSpec(valueType);
      typeNameSpec = new SqlMapTypeNameSpec(keyTypeSpec, valueTypeSpec, SqlParserPos.ZERO);
    } else {
      throw new UnsupportedOperationException(
          "Unsupported type when convertTypeToSpec: " + typeName);
    }

    // REVIEW jvs 28-Dec-2004:  discriminate between precision/scale
    // zero and unspecified?

    // REVIEW angel 11-Jan-2006:
    // Use neg numbers to indicate unspecified precision/scale

    return new SqlDataTypeSpec(typeNameSpec, SqlParserPos.ZERO)
        .withNullable(type.isNullable());
  }

  /**
   * Converts an instance of RelDataType to an instance of SqlDataTypeSpec.
   *
   * @param type type descriptor
   * @return corresponding parse representation
   */
  public static SqlDataTypeSpec convertTypeToSpec(RelDataType type) {
    // TODO jvs 28-Dec-2004:  collation
    String charSetName = inCharFamily(type) ? type.getCharset().name() : null;
    return convertTypeToSpec(type, charSetName,
        RelDataType.PRECISION_NOT_SPECIFIED, RelDataType.SCALE_NOT_SPECIFIED);
  }

  public static RelDataType createMultisetType(
      RelDataTypeFactory typeFactory,
      RelDataType type,
      boolean nullable) {
    RelDataType ret = typeFactory.createMultisetType(type, -1);
    return typeFactory.createTypeWithNullability(ret, nullable);
  }

  public static RelDataType createArrayType(
      RelDataTypeFactory typeFactory,
      RelDataType type,
      boolean nullable) {
    RelDataType ret = typeFactory.createArrayType(type, -1);
    return typeFactory.createTypeWithNullability(ret, nullable);
  }

  public static RelDataType createMapType(
      RelDataTypeFactory typeFactory,
      RelDataType keyType,
      RelDataType valueType,
      boolean nullable) {
    RelDataType ret = typeFactory.createMapType(keyType, valueType);
    return typeFactory.createTypeWithNullability(ret, nullable);
  }

  /** Creates a MAP type from a record type. The record type must have exactly
   * two fields. */
  public static RelDataType createMapTypeFromRecord(
      RelDataTypeFactory typeFactory, RelDataType type) {
    checkArgument(type.getFieldCount() == 2,
        "MAP requires exactly two fields, got %s; row type %s",
        type.getFieldCount(), type);
    return createMapType(typeFactory, type.getFieldList().get(0).getType(),
        type.getFieldList().get(1).getType(), false);
  }

  /** Creates a ROW type from a map type. The record type will have two fields. */
  public static RelDataType createRecordTypeFromMap(
      RelDataTypeFactory typeFactory, RelDataType type) {
    RelDataType keyType = requireNonNull(type.getKeyType(), () -> "keyType of " + type);
    RelDataType valueType = requireNonNull(type.getValueType(), () -> "valueType of " + type);
    return typeFactory.createStructType(
        Arrays.asList(keyType, valueType), Arrays.asList("f0", "f1"));
  }

  /**
   * Adds collation and charset to a character type, returns other types
   * unchanged.
   *
   * @param type        Type
   * @param typeFactory Type factory
   * @return Type with added charset and collation, or unchanged type if it is
   * not a char type.
   */
  public static RelDataType addCharsetAndCollation(
      RelDataType type,
      RelDataTypeFactory typeFactory) {
    if (!inCharFamily(type)) {
      return type;
    }
    Charset charset = type.getCharset();
    if (charset == null) {
      charset = typeFactory.getDefaultCharset();
    }
    SqlCollation collation = type.getCollation();
    if (collation == null) {
      collation = SqlCollation.IMPLICIT;
    }

    // todo: should get the implicit collation from repository
    //   instead of null
    type =
        typeFactory.createTypeWithCharsetAndCollation(
            type,
            charset,
            collation);
    SqlValidatorUtil.checkCharsetAndCollateConsistentIfCharType(type);
    return type;
  }

  /**
   * Returns whether two types are equal, ignoring nullability.
   *
   * <p>They need not come from the same factory.
   *
   * @param factory Type factory
   * @param type1   First type
   * @param type2   Second type
   * @return whether types are equal, ignoring nullability
   */
  public static boolean equalSansNullability(
      RelDataTypeFactory factory,
      RelDataType type1,
      RelDataType type2) {
    if (type1.isNullable() == type2.isNullable()) {
      return type1.equals(type2);
    }
    return type1.equals(
        factory.createTypeWithNullability(type2, type1.isNullable()));
  }

  /**
   * This is a poorman's
   * {@link #equalSansNullability(RelDataTypeFactory, RelDataType, RelDataType)}.
   *
   * <p>We assume that "not null" is represented in the type's digest
   * as a trailing "NOT NULL" (case sensitive).
   *
   * <p>If you got a type factory, {@link #equalSansNullability(RelDataTypeFactory, RelDataType, RelDataType)}
   * is preferred.
   *
   * @param type1 First type
   * @param type2 Second type
   * @return true if the types are equal or the only difference is nullability
   */
  public static boolean equalSansNullability(RelDataType type1, RelDataType type2) {
    if (type1 == type2) {
      return true;
    }
    String x = type1.getFullTypeString();
    String y = type2.getFullTypeString();
    if (x.length() < y.length()) {
      String c = x;
      x = y;
      y = c;
    }

    return (x.length() == y.length()
        || x.length() == y.length() + NON_NULLABLE_SUFFIX.length()
        && x.endsWith(NON_NULLABLE_SUFFIX)) && x.startsWith(y);
  }

  /**
   * Returns whether two collection types are equal, ignoring nullability.
   *
   * <p>They need not come from the same factory.
   *
   * @param factory       Type factory
   * @param type1         First type
   * @param type2         Second type
   * @return Whether types are equal, ignoring nullability
   */
  public static boolean equalAsCollectionSansNullability(
      RelDataTypeFactory factory,
      RelDataType type1,
      RelDataType type2) {
    checkArgument(isCollection(type1), "Input type1 must be collection type");
    checkArgument(isCollection(type2), "Input type2 must be collection type");

    return (type1 == type2)
        || (type1.getSqlTypeName() == type2.getSqlTypeName()
          && equalSansNullability(
              factory, getComponentTypeOrThrow(type1), getComponentTypeOrThrow(type2)));
  }

  /**
   * Returns whether two map types are equal, ignoring nullability.
   *
   * <p>They need not come from the same factory.
   *
   * @param factory       Type factory
   * @param type1         First type
   * @param type2         Second type
   * @return Whether types are equal, ignoring nullability
   */
  public static boolean equalAsMapSansNullability(
      RelDataTypeFactory factory,
      RelDataType type1,
      RelDataType type2) {
    checkArgument(isMap(type1), "Input type1 must be map type");
    checkArgument(isMap(type2), "Input type2 must be map type");

    MapSqlType mType1 = (MapSqlType) type1;
    MapSqlType mType2 = (MapSqlType) type2;
    return (type1 == type2)
        || (equalSansNullability(factory, mType1.getKeyType(), mType2.getKeyType())
          && equalSansNullability(factory, mType1.getValueType(), mType2.getValueType()));
  }

  /**
   * Returns whether two struct types are equal, ignoring nullability.
   *
   * <p>They do not need to come from the same factory.
   *
   * @param factory       Type factory
   * @param type1         First type
   * @param type2         Second type
   * @param nameMatcher   Name matcher used to compare the field names, if null,
   *                      the field names are also ignored
   *
   * @return Whether types are equal, ignoring nullability
   */
  public static boolean equalAsStructSansNullability(
      RelDataTypeFactory factory,
      RelDataType type1,
      RelDataType type2,
      @Nullable SqlNameMatcher nameMatcher) {
    checkArgument(type1.isStruct(), "Input type1 must be struct type");
    checkArgument(type2.isStruct(), "Input type2 must be struct type");

    if (type1 == type2) {
      return true;
    }

    if (type1.getFieldCount() != type2.getFieldCount()) {
      return false;
    }

    for (Pair<RelDataTypeField, RelDataTypeField> pair
        : Pair.zip(type1.getFieldList(), type2.getFieldList())) {
      if (nameMatcher != null
          && !nameMatcher.matches(pair.left.getName(), pair.right.getName())) {
        return false;
      }
      if (!equalSansNullability(factory, pair.left.getType(), pair.right.getType())) {
        return false;
      }
    }

    return true;
  }

  /**
   * Returns the ordinal of a given field in a record type, or -1 if the field
   * is not found.
   *
   * <p>The {@code fieldName} is always simple, if the field is nested within a record field,
   * returns index of the outer field instead. i.g. for row type
   * (a int, b (b1 bigint, b2 varchar(20) not null)), returns 1 for both simple name "b1" and "b2".
   *
   * @param type      Record type
   * @param fieldName Name of field
   * @return Ordinal of field
   */
  public static int findField(RelDataType type, String fieldName) {
    List<RelDataTypeField> fields = type.getFieldList();
    for (int i = 0; i < fields.size(); i++) {
      RelDataTypeField field = fields.get(i);
      if (field.getName().equals(fieldName)) {
        return i;
      }
      final RelDataType fieldType = field.getType();
      if (fieldType.isStruct() && findField(fieldType, fieldName) != -1) {
        return i;
      }
    }
    return -1;
  }

  /**
   * Selects data types of the specified fields from an input row type.
   * This is useful when identifying data types of a function that is going
   * to operate on inputs that are specified as field ordinals (e.g.
   * aggregate calls).
   *
   * @param rowType input row type
   * @param requiredFields ordinals of the projected fields
   * @return list of data types that are requested by requiredFields
   */
  public static List<RelDataType> projectTypes(final RelDataType rowType,
      final List<? extends Number> requiredFields) {
    final List<RelDataTypeField> fields = rowType.getFieldList();

    return new AbstractList<RelDataType>() {
      @Override public RelDataType get(int index) {
        return fields.get(requiredFields.get(index).intValue()).getType();
      }

      @Override public int size() {
        return requiredFields.size();
      }
    };
  }

  /**
   * Records a struct type with no fields.
   *
   * @param typeFactory Type factory
   * @return Struct type with no fields
   */
  public static RelDataType createEmptyStructType(
      RelDataTypeFactory typeFactory) {
    return typeFactory.createStructType(
        ImmutableList.of(),
        ImmutableList.of());
  }

  /** Returns whether a type is flat. It is not flat if it is a record type that
   * has one or more fields that are themselves record types. */
  public static boolean isFlat(RelDataType type) {
    if (type.isStruct()) {
      for (RelDataTypeField field : type.getFieldList()) {
        if (field.getType().isStruct()) {
          return false;
        }
      }
    }
    return true;
  }

  /**
   * Returns whether two types are comparable. They need to be scalar types of
   * the same family, or struct types whose fields are pairwise comparable.
   * Note that types in the CHARACTER family are comparable with many other types
   * (see {@link #canConvertStringInCompare}).
   *
   * @param type1 First type
   * @param type2 Second type
   * @return Whether types are comparable
   */
  public static boolean isComparable(RelDataType type1, RelDataType type2) {
    final RelDataTypeFamily family1 = family(type1);
    final RelDataTypeFamily family2 = family(type2);

    // If one of the arguments is of type 'NULL', return true.
    if (family1 == SqlTypeFamily.NULL
        || family2 == SqlTypeFamily.NULL) {
      return true;
    }

    if (type1.isStruct() != type2.isStruct()) {
      return false;
    }

    if (type1.isStruct()) {
      int n = type1.getFieldCount();
      if (n != type2.getFieldCount()) {
        return false;
      }
      for (Pair<RelDataTypeField, RelDataTypeField> pair
          : Pair.zip(type1.getFieldList(), type2.getFieldList())) {
        if (!isComparable(pair.left.getType(), pair.right.getType())) {
          return false;
        }
      }
      return true;
    }

    SqlTypeName type1Name = type1.getSqlTypeName();
    SqlTypeName type2Name = type2.getSqlTypeName();
    if (type1Name == SqlTypeName.ARRAY || type1Name == SqlTypeName.MULTISET) {
      if (type2Name != type1Name) {
        return false;
      }
      RelDataType elementType1 = requireNonNull(type1.getComponentType());
      RelDataType elementType2 = requireNonNull(type2.getComponentType());
      return isComparable(elementType1, elementType2);
    }

    if (type1Name == SqlTypeName.MAP) {
      if (type2Name != type1Name) {
        return false;
      }
      RelDataType keyType1 = requireNonNull(type1.getKeyType());
      RelDataType keyType2 = requireNonNull(type2.getKeyType());
      RelDataType valueType1 = requireNonNull(type1.getValueType());
      RelDataType valueType2 = requireNonNull(type2.getValueType());
      return isComparable(keyType1, keyType2) && isComparable(valueType1, valueType2);
    }

    if (family1 == family2) {
      return true;
    }

    // If one of the arguments is of type 'ANY', return true.
    if (family1 == SqlTypeFamily.ANY
        || family2 == SqlTypeFamily.ANY) {
      return true;
    }

    // We can implicitly convert from character to date
    return family1 == SqlTypeFamily.CHARACTER
        && canConvertStringInCompare(family2)
        || family2 == SqlTypeFamily.CHARACTER
        && canConvertStringInCompare(family1);
  }

  /** Returns the least restrictive type T, such that a value of type T can be
   * compared with values of type {@code type1} and {@code type2} using
   * {@code =}. */
  public static @Nullable RelDataType leastRestrictiveForComparison(
      RelDataTypeFactory typeFactory, RelDataType type1, RelDataType type2) {
    final RelDataType type =
        typeFactory.leastRestrictive(ImmutableList.of(type1, type2));
    if (type != null) {
      return type;
    }
    final RelDataTypeFamily family1 = family(type1);
    final RelDataTypeFamily family2 = family(type2);

    // If one of the arguments is of type 'ANY', we can compare.
    if (family1 == SqlTypeFamily.ANY) {
      return type2;
    }
    if (family2 == SqlTypeFamily.ANY) {
      return type1;
    }

    // If one of the arguments is of type 'NULL', we can compare.
    if (family1 == SqlTypeFamily.NULL) {
      return type2;
    }
    if (family2 == SqlTypeFamily.NULL) {
      return type1;
    }

    // We can implicitly convert from character to date, numeric, etc.
    if (family1 == SqlTypeFamily.CHARACTER
        && canConvertStringInCompare(family2)) {
      return type2;
    }
    if (family2 == SqlTypeFamily.CHARACTER
        && canConvertStringInCompare(family1)) {
      return type1;
    }

    return null;
  }

  protected static RelDataTypeFamily family(RelDataType type) {
    // REVIEW jvs 2-June-2005:  This is needed to keep
    // the Saffron type system happy.
    RelDataTypeFamily family = null;
    if (type.getSqlTypeName() != null) {
      family = type.getSqlTypeName().getFamily();
    }
    if (family == null) {
      family = type.getFamily();
    }
    return family;
  }

  /**
   * Returns whether all types in a collection have the same family, as
   * determined by {@link #isSameFamily(RelDataType, RelDataType)}.
   *
   * @param types Types to check
   * @return true if all types are of the same family
   */
  public static boolean areSameFamily(Iterable<RelDataType> types) {
    final List<RelDataType> typeList = ImmutableList.copyOf(types);
    if (Sets.newHashSet(RexUtil.families(typeList)).size() < 2) {
      return true;
    }
    for (Pair<RelDataType, RelDataType> adjacent : Pair.adjacents(typeList)) {
      if (!isSameFamily(adjacent.left, adjacent.right)) {
        return false;
      }
    }
    return true;
  }

  /**
   * Returns whether two types are scalar types of the same family, or struct types whose fields
   * are pairwise of the same family.
   *
   * @param type1 First type
   * @param type2 Second type
   * @return Whether types have the same family
   */
  private static boolean isSameFamily(RelDataType type1, RelDataType type2) {
    if (type1.isStruct() != type2.isStruct()) {
      return false;
    }

    if (type1.isStruct()) {
      int n = type1.getFieldCount();
      if (n != type2.getFieldCount()) {
        return false;
      }
      for (Pair<RelDataTypeField, RelDataTypeField> pair
          : Pair.zip(type1.getFieldList(), type2.getFieldList())) {
        if (!isSameFamily(pair.left.getType(), pair.right.getType())) {
          return false;
        }
      }
      return true;
    }

    final RelDataTypeFamily family1 = family(type1);
    final RelDataTypeFamily family2 = family(type2);
    return family1 == family2;
  }

  /** Returns whether a character data type can be implicitly converted to a
   * given family in a compare operation. */
  private static boolean canConvertStringInCompare(RelDataTypeFamily family) {
    if (family instanceof SqlTypeFamily) {
      SqlTypeFamily sqlTypeFamily = (SqlTypeFamily) family;
      switch (sqlTypeFamily) {
      case DATE:
      case TIME:
      case TIMESTAMP:
      case INTERVAL_DAY_TIME:
      case INTERVAL_YEAR_MONTH:
      case NUMERIC:
      case APPROXIMATE_NUMERIC:
      case EXACT_NUMERIC:
      case INTEGER:
      case BOOLEAN:
        return true;
      default:
        break;
      }
    }
    return false;
  }

  /**
   * Checks whether a type represents Unicode character data.
   *
   * @param type type to test
   * @return whether type represents Unicode character data
   */
  public static boolean isUnicode(RelDataType type) {
    Charset charset = type.getCharset();
    if (charset == null) {
      return false;
    }
    return charset.name().startsWith("UTF");
  }

  /** Returns the larger of two precisions, treating
   * {@link RelDataType#PRECISION_NOT_SPECIFIED} as infinity. */
  public static int maxPrecision(int p0, int p1) {
    return (p0 == RelDataType.PRECISION_NOT_SPECIFIED
        || p0 >= p1
        && p1 != RelDataType.PRECISION_NOT_SPECIFIED) ? p0 : p1;
  }

  /** Returns whether a precision is greater or equal than another,
   * treating {@link RelDataType#PRECISION_NOT_SPECIFIED} as infinity. */
  public static int comparePrecision(int p0, int p1) {
    if (p0 == p1) {
      return 0;
    }
    if (p0 == RelDataType.PRECISION_NOT_SPECIFIED) {
      return 1;
    }
    if (p1 == RelDataType.PRECISION_NOT_SPECIFIED) {
      return -1;
    }
    return Integer.compare(p0, p1);
  }

  /** Returns whether a type is ARRAY. */
  public static boolean isArray(RelDataType type) {
    return type.getSqlTypeName() == SqlTypeName.ARRAY;
  }

  /** Returns whether a type is ROW. */
  public static boolean isRow(RelDataType type) {
    SqlTypeName typeName = type.getSqlTypeName();
    if (typeName == null) {
      return false;
    }
    return type.getSqlTypeName() == SqlTypeName.ROW;
  }

  /** Returns whether a type is MAP. */
  public static boolean isMap(RelDataType type) {
    SqlTypeName typeName = type.getSqlTypeName();
    if (typeName == null) {
      return false;
    }
    return type.getSqlTypeName() == SqlTypeName.MAP;
  }

  /** Returns whether a type is MULTISET. */
  public static boolean isMultiset(RelDataType type) {
    SqlTypeName typeName = type.getSqlTypeName();
    if (typeName == null) {
      return false;
    }
    return type.getSqlTypeName() == SqlTypeName.MULTISET;
  }

  /** Returns whether a type is ARRAY or MULTISET. */
  public static boolean isCollection(RelDataType type) {
    SqlTypeName typeName = type.getSqlTypeName();
    if (typeName == null) {
      return false;
    }
    return type.getSqlTypeName() == SqlTypeName.ARRAY
        || type.getSqlTypeName() == SqlTypeName.MULTISET;
  }

  /** Returns whether a type is CHARACTER. */
  public static boolean isCharacter(RelDataType type) {
    SqlTypeName typeName = type.getSqlTypeName();
    if (typeName == null) {
      return false;
    }
    return SqlTypeFamily.CHARACTER.contains(type);
  }

  /** Returns whether a type is a CHARACTER or contains a CHARACTER type.
   *
   * @deprecated Use {@link #hasCharacter(RelDataType)} */
  @Deprecated // to be removed before 2.0
  public static boolean hasCharactor(RelDataType type) {
    return hasCharacter(type);
  }

  /** Returns whether a type is a CHARACTER or contains a CHARACTER type. */
  public static boolean hasCharacter(RelDataType type) {
    if (isCharacter(type)) {
      return true;
    }
    if (isArray(type)) {
      return hasCharacter(getComponentTypeOrThrow(type));
    }
    return false;
  }

  /** Returns whether a type is STRING. */
  public static boolean isString(RelDataType type) {
    SqlTypeName typeName = type.getSqlTypeName();
    if (typeName == null) {
      return false;
    }
    return SqlTypeFamily.STRING.contains(type);
  }

  /** Returns whether a type is BOOLEAN. */
  public static boolean isBoolean(RelDataType type) {
    SqlTypeName typeName = type.getSqlTypeName();
    if (typeName == null) {
      return false;
    }
    return SqlTypeFamily.BOOLEAN.contains(type);
  }

  /** Returns whether a type is BINARY. */
  public static boolean isBinary(RelDataType type) {
    SqlTypeName typeName = type.getSqlTypeName();
    if (typeName == null) {
      return false;
    }
    return SqlTypeFamily.BINARY.contains(type);
  }

  /** Returns whether a type is atomic (datetime, numeric, string or
   * BOOLEAN). */
  public static boolean isAtomic(RelDataType type) {
    SqlTypeName typeName = type.getSqlTypeName();
    if (typeName == null) {
      return false;
    }
    return SqlTypeUtil.isDatetime(type)
        || SqlTypeUtil.isNumeric(type)
        || SqlTypeUtil.isString(type)
        || SqlTypeUtil.isBoolean(type)
        || typeName == SqlTypeName.UUID
        || typeName == SqlTypeName.VARIANT;
  }

  /** Returns a DECIMAL type with the maximum precision for the current
   * type system. */
  @SuppressWarnings("deprecation") // [CALCITE-6598]
  public static RelDataType getMaxPrecisionScaleDecimal(RelDataTypeFactory factory) {
    int maxPrecision = factory.getTypeSystem().getMaxNumericPrecision();
    int maxScale = factory.getTypeSystem().getMaxNumericScale();
    // scale should not greater than precision.
    int scale = Math.min(maxPrecision / 2, maxScale);
    return factory.createSqlType(SqlTypeName.DECIMAL, maxPrecision, scale);
  }

  /**
   * Keeps only the last N fields and returns the new struct type.
   */
  public static RelDataType extractLastNFields(RelDataTypeFactory typeFactory,
      RelDataType type, int numToKeep) {
    assert type.isStruct();
    assert type.getFieldCount() >= numToKeep;
    final int fieldsCnt = type.getFieldCount();
    return typeFactory.createStructType(
        type.getFieldList().subList(fieldsCnt - numToKeep, fieldsCnt));
  }

  /**
   * Returns whether the decimal value can be represented without information loss
   * using the specified type.
   * For example, 1111.11
   * - cannot be represented exactly using DECIMAL(3, 1) since it overflows.
   * - cannot be represented exactly using DECIMAL(6, 3) since it overflows.
   * - cannot be represented exactly using DECIMAL(6, 1) since it requires rounding.
   * - can be represented exactly using DECIMAL(6, 2)
   *
   * @param value  A decimal value
   * @param toType A DECIMAL type.
   * @return whether the value is valid for the type
   */
  public static boolean canBeRepresentedExactly(@Nullable BigDecimal value, RelDataType toType) {
    assert toType.getSqlTypeName() == SqlTypeName.DECIMAL;
    if (value == null) {
      return true;
    }
    value = value.stripTrailingZeros();
    if (value.scale() < 0) {
      // Negative scale, convert to 0 scale.
      // Rounding mode is irrelevant, since value is integer
      value = value.setScale(0, RoundingMode.DOWN);
    }
    final int intDigits = value.precision() - value.scale();
    final int maxIntDigits = toType.getPrecision() - toType.getScale();
    return (intDigits <= maxIntDigits) && (value.scale() <= toType.getScale());
  }

  /**
   * Returns whether the decimal value is valid for the type. For example, 1111.11 is not
   * valid for DECIMAL(3, 1) since it overflows.
   *
   * @param value Value of literal
   * @param toType Type of the literal
   * @return whether the value is valid for the type
   */
  public static boolean isValidDecimalValue(@Nullable BigDecimal value, RelDataType toType) {
    if (value == null) {
      return true;
    }
    switch (toType.getSqlTypeName()) {
    case DECIMAL:
      final int intDigits = value.precision() - value.scale();
      final int maxIntDigits = toType.getPrecision() - toType.getScale();
      return intDigits <= maxIntDigits;
    default:
      return true;
    }
  }

  /** Strips MEASURE wrappers from a type.
   *
   * <p>For example:
   * <ul>
   *   <li>"{@code INTEGER}" remains "{@code INTEGER}";
   *   <li>"{@code MEASURE<DECIMAL>}" becomes "{@code DECIMAL}";
   *   <li>"{@code (empno INTEGER NOT NULL, rating MEASURE<DECIMAL>)}"
   *     becomes "{@code (empno INTEGER NOT NULL, rating DECIMAL)}".
   * </ul>
   */
  public static RelDataType fromMeasure(RelDataTypeFactory typeFactory,
      RelDataType type) {
    if (type.isStruct()) {
      final RelDataTypeFactory.Builder builder = typeFactory.builder();
      int changeCount = 0;
      for (RelDataTypeField field : type.getFieldList()) {
        final RelDataType type2 = fromMeasure(typeFactory, field.getType());
        if (type2 != field.getType()) {
          ++changeCount;
        }
        builder.add(field.getName(), type2);
      }
      // Avoid the effort of re-creating the same type
      return changeCount == 0 ? type : builder.build();
    }
    if (type.isMeasure()) {
      return ((MeasureSqlType) type).types.get(0);
    }
    return type;
  }
}
